import { TWEEN } from '../js/TWEEN';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import { CSS2DObject, CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
import { EventList } from './EventList'
import { WEBGL } from 'three/examples/jsm/WebGL.js';
import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer';

const { Clock, PMREMGenerator, Scene, Raycaster, Vector2, WebGLRenderer, PerspectiveCamera, TextureLoader, RepeatWrapping, Vector3, Mesh, Line, Spherical, Quaternion, UnsignedByteType, Color, sRGBEncoding } = require("three");

const baseurl = ''

function ISMobile() {
    //移动SSAO动态开启
    return navigator.userAgent.match(/(Mac|phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i);
}

export class ThreeBase {
    constructor(dom) {
        this.scene2= new Scene();
        this.TWEEN = TWEEN;
        this.isMobile = ISMobile();
        this.eventList = new EventList();
        this.container = (dom instanceof Element) ? dom : document.querySelector(dom);
        this.scene = new Scene();
        this.raycaster = new Raycaster();
        this.raycaster.params.Line = { threshold: 0 };
        this.mouse = new Vector2();
        this.initWidthAndHeight();
        this.clock = new Clock();
        this.initCamera();
        this.initcss2DRenderer();
        this.initWebGLRenderer();
    }

    getCubeMapTexture(path) {
        // no envmap
        if (!path) return Promise.resolve({ envMap: null });
        return new Promise((resolve, reject) => {
            new RGBELoader()
                .setDataType(UnsignedByteType)
                .load(path, (texture) => {
                    const envMap = this.pmremGenerator.fromEquirectangular(texture).texture;
                    this.pmremGenerator.dispose();
                    resolve({ envMap });
                }, undefined, reject);
        });
    }

    initCamera() {
        this.camera = new PerspectiveCamera(40, this.width / this.height, 1, 10000);
        this.camera.frustumCulled = false;
        this.camera.updateProjectionMatrix();
        this.scene.add(this.camera);
    }

    initCSS3DRenderer() {
        this.css3dRenderer = new CSS3DRenderer();
        this.css3dRenderer.setSize(this.width,this.height);
        this.css3dRenderer.domElement.style.position = 'fixed';
        this.container.appendChild(this.css3dRenderer.domElement);
    }

    initcss2DRenderer() {
        this.CSS2DObject = CSS2DObject;
        this.css2DRenderer = new CSS2DRenderer();
        this.css2DRenderer.setSize(this.container.offsetWidth, this.container.offsetHeight);
        this.css2DRenderer.domElement.style.position = 'fixed';

        this.container.appendChild(this.css2DRenderer.domElement);
    }


    initWebGLRenderer() {
        this.renderer = new WebGLRenderer({ antialias: true });
        WEBGL.isWebGL2Available() && (this.renderer.capabilities.isWebGL2 = true);
        this.renderer.outputEncoding = sRGBEncoding;
        this.renderer.setClearColor(0xffffff);
        this.renderer.toneMappingExposure = 1.0;
        this.renderer.setSize(this.width, this.height);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.autoClear = false;
        this.renderer.domElement.id = 'canvas';
        this.container.appendChild(this.renderer.domElement);
        this.composer = new EffectComposer(this.renderer);
        this.pmremGenerator = new PMREMGenerator(this.renderer);
        this.pmremGenerator.compileEquirectangularShader();
    }


    initWidthAndHeight() {
        this.width = this.container.offsetWidth;
        this.height = this.container.offsetHeight;
    }

    async loadObjModel(path, obj, mtl) {
        let materials = await new MTLLoader().loadAsync(path + mtl);
        let group = await new OBJLoader().setMaterials(materials).loadAsync(path + obj);
        group.dname = obj;
        return group;
    }

    loadGLTFModel(path) {
        let loader = null;
        return new Promise(resolve => {
            try {
                let draco = (new DRACOLoader()).setDecoderPath(baseurl + '/draco/gltf/');
                loader = new GLTFLoader()
                    .setDRACOLoader(draco)
                    .setKTX2Loader(new KTX2Loader().detectSupport(this.renderer))
                    .load(path, obj => {
                        resolve(obj);
                    }, ({ loaded, total }) => {
                        console.info('loaded:' + loaded, 'total:' + total)
                    }, error => {
                        console.log(error);
                        alert('导入文件格式不正确！');
                    });
            } catch (e) {
                alert('文件格式不正确！');
            }
        });
    }

    loadGLTF(path, fileName, callback) {
        let draco = (new DRACOLoader()).setDecoderPath(baseurl + '/draco/gltf/');
        (new GLTFLoader().setDRACOLoader(draco).setPath(path)).load(fileName, callback, ({ loaded, total }) => {
            console.info('loaded:' + loaded, 'total:' + total);
            draco = null;
        });
    }

    loadModel(path, fileName) {
        let fileNames = fileName.split('.');
        return new Promise(resolve => {
            let suffix = fileNames[fileNames.length - 1];
            let loader = null;
            switch (suffix.toLowerCase()) {
                case 'gltf':
                case 'glb':
                    loader = new GLTFLoader()
                        .setDRACOLoader((new DRACOLoader()).setDecoderPath(baseurl + '/draco/gltf/'))
                        .setPath(path);
                    break;
            }
            loader.load(fileName, obj => {
                resolve(obj);
                obj = null;
                loader = null;
                console.log('destory');
            }, ({ loaded, total }) => {
                console.info('loaded:' + loaded, 'total:' + total)
            });
        });
    }

    loadTexture(url) {
        let t = new TextureLoader().load(url);
        t.wrapS = t.wrapT = RepeatWrapping;
        return t;
    }

    getTouchRaycastersByObject(e, objects) {
        const canvas = this.renderer.domElement;
        this.mouse.x = ((e.touches[0].pageX - canvas.getBoundingClientRect().left) / canvas.offsetWidth) * 2 - 1;
        this.mouse.y = -((e.touches[0].pageY - canvas.getBoundingClientRect().top) / canvas.offsetHeight) * 2 + 1;
        this.raycaster.setFromCamera(this.mouse, this.camera);
        return this.raycaster.intersectObjects(objects, true)
    }

    getRaycastersByObject(e, objects) {
        const canvas = this.renderer.domElement;
        this.mouse.x = ((e.clientX - canvas.getBoundingClientRect().left) / canvas.offsetWidth) * 2 - 1;
        this.mouse.y = -((e.clientY - canvas.getBoundingClientRect().top) / canvas.offsetHeight) * 2 + 1;
        this.raycaster.setFromCamera(this.mouse, this.camera);

        return this.raycaster.intersectObjects(objects, true)
    }

    destoryScene() {
        this.renderer.forceContextLoss();
        document.body.removeChild(this.stats.domElement);
        this.dispose(this.scene);
    }


    dispose(scene) {
        let arr = [];
        let css2ds = [];
        scene.traverse(element => {
            if (element instanceof Mesh || element instanceof Line) {
                arr.push(element);
            }
            if (element instanceof CSS2DObject) {
                css2ds.push(element);
            }
        }, true);

        for (let i = 0; i < css2ds.length; i++) {
            const element = css2ds[i];
            scene.remove(element);
        }

        css2ds.length = 0;

        for (let i = 0; i < arr.length; i++) {
            const element = arr[i];
            if (element instanceof Mesh || element instanceof Line) {
                element.parent.remove(element);
                if (element.material instanceof Array) {
                    for (let k = 0; k < element.material.length; k++) {
                        const m = element.material[k];
                        m.map && m.map.dispose();
                        m.envMap && m.envMap.dispose();
                        m.dispose();
                    }
                } else {
                    element.material.map && element.material.map.dispose();
                    element.material.envMap && element.material.envMap.dispose();
                    element.material.dispose();
                }
                element.geometry.dispose();
            }
        };
        arr.length = 0, arr = null;
        this.scene.remove(scene);
    }


    /**
     * @param {*} current
     * @param {*} target
     * @param {*} interval
     * @param {*} animation
     * @param {*} onUpdate
     * @param {*} complete
     */
    tweenAnimate(current, target, interval, animation, onUpdate, onComplete, auto, clearBefore) {
        clearBefore && TWEEN.removeAll();
        const tween = new TWEEN.Tween(current)
            .to(target, interval)
            .easing(TWEEN.Easing.Linear.None)
            .onUpdate(() => onUpdate && onUpdate(current))
            .onComplete(() =>{
                TWEEN.remove(tween);
                onComplete && onComplete(current)
            });
        auto && tween.start();
        return tween;
    }

    tweenAnimate2(current, target, interval, animation, onUpdate, complete) {
        var animate = animation ? animation : TWEEN.Easing.Linear.None;
        let tween = new TWEEN.Tween(current).to(target, interval).easing(animate);
        onUpdate && tween.onUpdate(() => onUpdate());
        complete && tween.onComplete(() => complete());

        return tween;
    }

    /**
     * 平移镜头
     * @param {*} camera 相机
     * @param {*} controls 控制器
     * @param {*} point 目标位置
     * @param {*} distance 距离
     * @param {*} speed 平移速度
     */
    panCamera(camera, controls, point, distance, speed = 500) {
        this.tweenAnimate(
            [...controls.target.toArray(),
                ...this.camera.position.toArray()
            ], [...point.toArray(),
                ...(new Vector3()
                    .addVectors(point, new Vector3()
                        .subVectors(new Vector3()
                            .addVectors(this.camera.position, new Vector3()
                                .subVectors(point, controls.target)), point)
                        .normalize().multiplyScalar(distance || camera.position.distanceTo(controls.target)))).toArray()
            ],
            speed, TWEEN.Easing.Sinusoidal.InOut, current => {
                controls.target.fromArray(current, 0);
                camera.position.fromArray(current, 3);
                controls.update();
            }, null, true, true);
    }

    /**
     * 获取相机到目标位置固定距离的坐标
     * @param {*} target 目标位置
     * @param {*} distance 固定距离
     */
    getPositionByDistance(target, distance) {
        var spherical = new Spherical();
        var offset = new Vector3();
        var quat = new Quaternion().setFromUnitVectors(this.camera.up, new Vector3(0, 1, 0));
        var quatInverse = quat.clone().inverse();
        offset.copy(this.camera.position).sub(target);
        // rotate offset to "y-axis-is-up" space
        offset.applyQuaternion(quat);
        // angle from z-axis around y-axis
        spherical.setFromVector3(offset);
        spherical.radius = distance;
        spherical.phi = Math.PI / 2;
        offset.setFromSpherical(spherical);
        offset.applyQuaternion(quatInverse);
        return new Vector3().copy(target).add(offset);
    }

    /**
     * 模拟球极坐标获取位置  Y轴up
     * @param {*} origin 当前物体位置
     * @param {*} target 旋转点
     * @param {*} phi 绕x轴旋转的角度
     * @param {*} theta 绕y轴旋转的角度
     * @param {*} distance 当前物体与目标间的距离
     */
    getPositionBySpherical(origin, target, phi, theta, distance) {
        var r = distance || origin.distanceTo(target);
        var x = r * Math.sin(phi) * Math.sin(theta) + target.x;
        var z = r * Math.sin(phi) * Math.cos(theta) + target.z;
        var y = r * Math.cos(phi) + target.y;

        return new Vector3(x, y, z);
    }
}